//Copyright (c) 2009 The Chromium Authors. All rights reserved.
//Use of this source code is governed by a BSD-style license that can be
//found in the LICENSE file.

// Various functions for helping debug WebGL apps.

WebGLDebugUtils = function() {

    /**
     * Wrapped logging function.
     * @param {string} msg Message to log.
     */
    var log = function(msg) {
      if (window.console && window.console.log) {
        window.console.log(msg);
      }
    };
    
    /**
     * Which arguements are enums.
     * @type {!Object.<number, string>}
     */
    var glValidEnumContexts = {
    
      // Generic setters and getters
    
      'enable': { 0:true },
      'disable': { 0:true },
      'getParameter': { 0:true },
    
      // Rendering
    
      'drawArrays': { 0:true },
      'drawElements': { 0:true, 2:true },
    
      // Shaders
    
      'createShader': { 0:true },
      'getShaderParameter': { 1:true },
      'getProgramParameter': { 1:true },
    
      // Vertex attributes
    
      'getVertexAttrib': { 1:true },
      'vertexAttribPointer': { 2:true },
    
      // Textures
    
      'bindTexture': { 0:true },
      'activeTexture': { 0:true },
      'getTexParameter': { 0:true, 1:true },
      'texParameterf': { 0:true, 1:true },
      'texParameteri': { 0:true, 1:true, 2:true },
      'texImage2D': { 0:true, 2:true, 6:true, 7:true },
      'texSubImage2D': { 0:true, 6:true, 7:true },
      'copyTexImage2D': { 0:true, 2:true },
      'copyTexSubImage2D': { 0:true },
      'generateMipmap': { 0:true },
    
      // Buffer objects
    
      'bindBuffer': { 0:true },
      'bufferData': { 0:true, 2:true },
      'bufferSubData': { 0:true },
      'getBufferParameter': { 0:true, 1:true },
    
      // Renderbuffers and framebuffers
    
      'pixelStorei': { 0:true, 1:true },
      'readPixels': { 4:true, 5:true },
      'bindRenderbuffer': { 0:true },
      'bindFramebuffer': { 0:true },
      'checkFramebufferStatus': { 0:true },
      'framebufferRenderbuffer': { 0:true, 1:true, 2:true },
      'framebufferTexture2D': { 0:true, 1:true, 2:true },
      'getFramebufferAttachmentParameter': { 0:true, 1:true, 2:true },
      'getRenderbufferParameter': { 0:true, 1:true },
      'renderbufferStorage': { 0:true, 1:true },
    
      // Frame buffer operations (clear, blend, depth test, stencil)
    
      'clear': { 0:true },
      'depthFunc': { 0:true },
      'blendFunc': { 0:true, 1:true },
      'blendFuncSeparate': { 0:true, 1:true, 2:true, 3:true },
      'blendEquation': { 0:true },
      'blendEquationSeparate': { 0:true, 1:true },
      'stencilFunc': { 0:true },
      'stencilFuncSeparate': { 0:true, 1:true },
      'stencilMaskSeparate': { 0:true },
      'stencilOp': { 0:true, 1:true, 2:true },
      'stencilOpSeparate': { 0:true, 1:true, 2:true, 3:true },
    
      // Culling
    
      'cullFace': { 0:true },
      'frontFace': { 0:true },
    };
    
    /**
     * Map of numbers to names.
     * @type {Object}
     */
    var glEnums = null;
    
    /**
     * Initializes this module. Safe to call more than once.
     * @param {!WebGLRenderingContext} ctx A WebGL context. If
     *    you have more than one context it doesn't matter which one
     *    you pass in, it is only used to pull out constants.
     */
    function init(ctx) {
      if (glEnums == null) {
        glEnums = { };
        for (var propertyName in ctx) {
          if (typeof ctx[propertyName] == 'number') {
            glEnums[ctx[propertyName]] = propertyName;
          }
        }
      }
    }
    
    /**
     * Checks the utils have been initialized.
     */
    function checkInit() {
      if (glEnums == null) {
        throw 'WebGLDebugUtils.init(ctx) not called';
      }
    }
    
    /**
     * Returns true or false if value matches any WebGL enum
     * @param {*} value Value to check if it might be an enum.
     * @return {boolean} True if value matches one of the WebGL defined enums
     */
    function mightBeEnum(value) {
      checkInit();
      return (glEnums[value] !== undefined);
    }
    
    /**
     * Gets an string version of an WebGL enum.
     *
     * Example:
     *   var str = WebGLDebugUtil.glEnumToString(ctx.getError());
     *
     * @param {number} value Value to return an enum for
     * @return {string} The string version of the enum.
     */
    function glEnumToString(value) {
      checkInit();
      var name = glEnums[value];
      return (name !== undefined) ? name :
          ("*UNKNOWN WebGL ENUM (0x" + value.toString(16) + ")");
    }
    
    /**
     * Returns the string version of a WebGL argument.
     * Attempts to convert enum arguments to strings.
     * @param {string} functionName the name of the WebGL function.
     * @param {number} argumentIndx the index of the argument.
     * @param {*} value The value of the argument.
     * @return {string} The value as a string.
     */
    function glFunctionArgToString(functionName, argumentIndex, value) {
      var funcInfo = glValidEnumContexts[functionName];
      if (funcInfo !== undefined) {
        if (funcInfo[argumentIndex]) {
          return glEnumToString(value);
        }
      }
      return value.toString();
    }
    
    /**
     * Given a WebGL context returns a wrapped context that calls
     * gl.getError after every command and calls a function if the
     * result is not gl.NO_ERROR.
     *
     * @param {!WebGLRenderingContext} ctx The webgl context to
     *        wrap.
     * @param {!function(err, funcName, args): void} opt_onErrorFunc
     *        The function to call when gl.getError returns an
     *        error. If not specified the default function calls
     *        console.log with a message.
     */
    function makeDebugContext(ctx, opt_onErrorFunc) {
      init(ctx);
      opt_onErrorFunc = opt_onErrorFunc || function(err, functionName, args) {
            // apparently we can't do args.join(",");
            var argStr = "";
            for (var ii = 0; ii < args.length; ++ii) {
              argStr += ((ii == 0) ? '' : ', ') +
                  glFunctionArgToString(functionName, ii, args[ii]);
            }
            log("WebGL error "+ glEnumToString(err) + " in "+ functionName +
                "(" + argStr + ")");
          };
    
      // Holds booleans for each GL error so after we get the error ourselves
      // we can still return it to the client app.
      var glErrorShadow = { };
    
      // Makes a function that calls a WebGL function and then calls getError.
      function makeErrorWrapper(ctx, functionName) {
        return function() {
          var result = ctx[functionName].apply(ctx, arguments);
          var err = ctx.getError();
          if (err != 0) {
            glErrorShadow[err] = true;
            opt_onErrorFunc(err, functionName, arguments);
          }
          return result;
        };
      }
    
      // Make a an object that has a copy of every property of the WebGL context
      // but wraps all functions.
      var wrapper = {};
      for (var propertyName in ctx) {
        if (typeof ctx[propertyName] == 'function') {
           wrapper[propertyName] = makeErrorWrapper(ctx, propertyName);
         } else {
           wrapper[propertyName] = ctx[propertyName];
         }
      }
    
      // Override the getError function with one that returns our saved results.
      wrapper.getError = function() {
        for (var err in glErrorShadow) {
          if (glErrorShadow[err]) {
            glErrorShadow[err] = false;
            return err;
          }
        }
        return ctx.NO_ERROR;
      };
    
      return wrapper;
    }
    
    function resetToInitialState(ctx) {
      var numAttribs = ctx.getParameter(ctx.MAX_VERTEX_ATTRIBS);
      var tmp = ctx.createBuffer();
      ctx.bindBuffer(ctx.ARRAY_BUFFER, tmp);
      for (var ii = 0; ii < numAttribs; ++ii) {
        ctx.disableVertexAttribArray(ii);
        ctx.vertexAttribPointer(ii, 4, ctx.FLOAT, false, 0, 0);
        ctx.vertexAttrib1f(ii, 0);
      }
      ctx.deleteBuffer(tmp);
    
      var numTextureUnits = ctx.getParameter(ctx.MAX_TEXTURE_IMAGE_UNITS);
      for (var ii = 0; ii < numTextureUnits; ++ii) {
        ctx.activeTexture(ctx.TEXTURE0 + ii);
        ctx.bindTexture(ctx.TEXTURE_CUBE_MAP, null);
        ctx.bindTexture(ctx.TEXTURE_2D, null);
      }
    
      ctx.activeTexture(ctx.TEXTURE0);
      ctx.useProgram(null);
      ctx.bindBuffer(ctx.ARRAY_BUFFER, null);
      ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null);
      ctx.bindFramebuffer(ctx.FRAMEBUFFER, null);
      ctx.bindRenderbuffer(ctx.RENDERBUFFER, null);
      ctx.disable(ctx.BLEND);
      ctx.disable(ctx.CULL_FACE);
      ctx.disable(ctx.DEPTH_TEST);
      ctx.disable(ctx.DITHER);
      ctx.disable(ctx.SCISSOR_TEST);
      ctx.blendColor(0, 0, 0, 0);
      ctx.blendEquation(ctx.FUNC_ADD);
      ctx.blendFunc(ctx.ONE, ctx.ZERO);
      ctx.clearColor(0, 0, 0, 0);
      ctx.clearDepth(1);
      ctx.clearStencil(-1);
      ctx.colorMask(true, true, true, true);
      ctx.cullFace(ctx.BACK);
      ctx.depthFunc(ctx.LESS);
      ctx.depthMask(true);
      ctx.depthRange(0, 1);
      ctx.frontFace(ctx.CCW);
      ctx.hint(ctx.GENERATE_MIPMAP_HINT, ctx.DONT_CARE);
      ctx.lineWidth(1);
      ctx.pixelStorei(ctx.PACK_ALIGNMENT, 4);
      ctx.pixelStorei(ctx.UNPACK_ALIGNMENT, 4);
      ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, false);
      ctx.pixelStorei(ctx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
      // TODO: Delete this IF.
      if (ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL) {
        ctx.pixelStorei(ctx.UNPACK_COLORSPACE_CONVERSION_WEBGL, ctx.BROWSER_DEFAULT_WEBGL);
      }
      ctx.polygonOffset(0, 0);
      ctx.sampleCoverage(1, false);
      ctx.scissor(0, 0, ctx.canvas.width, ctx.canvas.height);
      ctx.stencilFunc(ctx.ALWAYS, 0, 0xFFFFFFFF);
      ctx.stencilMask(0xFFFFFFFF);
      ctx.stencilOp(ctx.KEEP, ctx.KEEP, ctx.KEEP);
      ctx.viewport(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
      ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT);
    
      // TODO: This should NOT be needed but Firefox fails with 'hint'
      while(ctx.getError());
    }
    
    function makeLostContextSimulatingContext(ctx) {
      var wrapper_ = {};
      var contextId_ = 1;
      var contextLost_ = false;
      var resourceId_ = 0;
      var resourceDb_ = [];
      var onLost_ = undefined;
      var onRestored_ = undefined;
      var nextOnRestored_ = undefined;
    
      // Holds booleans for each GL error so can simulate errors.
      var glErrorShadow_ = { };
    
      function isWebGLObject(obj) {
        //return false;
        return (obj instanceof WebGLBuffer ||
                obj instanceof WebGLFramebuffer ||
                obj instanceof WebGLProgram ||
                obj instanceof WebGLRenderbuffer ||
                obj instanceof WebGLShader ||
                obj instanceof WebGLTexture);
      }
    
      function checkResources(args) {
        for (var ii = 0; ii < args.length; ++ii) {
          var arg = args[ii];
          if (isWebGLObject(arg)) {
            return arg.__webglDebugContextLostId__ == contextId_;
          }
        }
        return true;
      }
    
      function clearErrors() {
        var k = Object.keys(glErrorShadow_);
        for (var ii = 0; ii < k.length; ++ii) {
          delete glErrorShdow_[k];
        }
      }
    
      // Makes a function that simulates WebGL when out of context.
      function makeLostContextWrapper(ctx, functionName) {
        var f = ctx[functionName];
        return function() {
          // Only call the functions if the context is not lost.
          if (!contextLost_) {
            if (!checkResources(arguments)) {
              glErrorShadow_[ctx.INVALID_OPERATION] = true;
              return;
            }
            var result = f.apply(ctx, arguments);
            return result;
          }
        };
      }
    
      for (var propertyName in ctx) {
        if (typeof ctx[propertyName] == 'function') {
           wrapper_[propertyName] = makeLostContextWrapper(ctx, propertyName);
         } else {
           wrapper_[propertyName] = ctx[propertyName];
         }
      }
    
      function makeWebGLContextEvent(statusMessage) {
        return {statusMessage: statusMessage};
      }
    
      function freeResources() {
        for (var ii = 0; ii < resourceDb_.length; ++ii) {
          var resource = resourceDb_[ii];
          if (resource instanceof WebGLBuffer) {
            ctx.deleteBuffer(resource);
          } else if (resource instanceof WebctxFramebuffer) {
            ctx.deleteFramebuffer(resource);
          } else if (resource instanceof WebctxProgram) {
            ctx.deleteProgram(resource);
          } else if (resource instanceof WebctxRenderbuffer) {
            ctx.deleteRenderbuffer(resource);
          } else if (resource instanceof WebctxShader) {
            ctx.deleteShader(resource);
          } else if (resource instanceof WebctxTexture) {
            ctx.deleteTexture(resource);
          }
        }
      }
    
      wrapper_.loseContext = function() {
        if (!contextLost_) {
          contextLost_ = true;
          ++contextId_;
          while (ctx.getError());
          clearErrors();
          glErrorShadow_[ctx.CONTEXT_LOST_WEBGL] = true;
          setTimeout(function() {
              if (onLost_) {
                onLost_(makeWebGLContextEvent("context lost"));
              }
            }, 0);
        }
      };
    
      wrapper_.restoreContext = function() {
        if (contextLost_) {
          if (onRestored_) {
            setTimeout(function() {
                freeResources();
                resetToInitialState(ctx);
                contextLost_ = false;
                if (onRestored_) {
                  var callback = onRestored_;
                  onRestored_ = nextOnRestored_;
                  nextOnRestored_ = undefined;
                  callback(makeWebGLContextEvent("context restored"));
                }
              }, 0);
          } else {
            throw "You can not restore the context without a listener"
          }
        }
      };
    
      // Wrap a few functions specially.
      wrapper_.getError = function() {
        if (!contextLost_) {
          var err;
          while (err = ctx.getError()) {
            glErrorShadow_[err] = true;
          }
        }
        for (var err in glErrorShadow_) {
          if (glErrorShadow_[err]) {
            delete glErrorShadow_[err];
            return err;
          }
        }
        return ctx.NO_ERROR;
      };
    
      var creationFunctions = [
        "createBuffer",
        "createFramebuffer",
        "createProgram",
        "createRenderbuffer",
        "createShader",
        "createTexture"
      ];
      for (var ii = 0; ii < creationFunctions.length; ++ii) {
        var functionName = creationFunctions[ii];
        wrapper_[functionName] = function(f) {
          return function() {
            if (contextLost_) {
              return null;
            }
            var obj = f.apply(ctx, arguments);
            obj.__webglDebugContextLostId__ = contextId_;
            resourceDb_.push(obj);
            return obj;
          };
        }(ctx[functionName]);
      }
    
      var functionsThatShouldReturnNull = [
        "getActiveAttrib",
        "getActiveUniform",
        "getBufferParameter",
        "getContextAttributes",
        "getAttachedShaders",
        "getFramebufferAttachmentParameter",
        "getParameter",
        "getProgramParameter",
        "getProgramInfoLog",
        "getRenderbufferParameter",
        "getShaderParameter",
        "getShaderInfoLog",
        "getShaderSource",
        "getTexParameter",
        "getUniform",
        "getUniformLocation",
        "getVertexAttrib"
      ];
      for (var ii = 0; ii < functionsThatShouldReturnNull.length; ++ii) {
        var functionName = functionsThatShouldReturnNull[ii];
        wrapper_[functionName] = function(f) {
          return function() {
            if (contextLost_) {
              return null;
            }
            return f.apply(ctx, arguments);
          }
        }(wrapper_[functionName]);
      }
    
      var isFunctions = [
        "isBuffer",
        "isEnabled",
        "isFramebuffer",
        "isProgram",
        "isRenderbuffer",
        "isShader",
        "isTexture"
      ];
      for (var ii = 0; ii < isFunctions.length; ++ii) {
        var functionName = isFunctions[ii];
        wrapper_[functionName] = function(f) {
          return function() {
            if (contextLost_) {
              return false;
            }
            return f.apply(ctx, arguments);
          }
        }(wrapper_[functionName]);
      }
    
      wrapper_.checkFramebufferStatus = function(f) {
        return function() {
          if (contextLost_) {
            return ctx.FRAMEBUFFER_UNSUPPORTED;
          }
          return f.apply(ctx, arguments);
        };
      }(wrapper_.checkFramebufferStatus);
    
      wrapper_.getAttribLocation = function(f) {
        return function() {
          if (contextLost_) {
            return -1;
          }
          return f.apply(ctx, arguments);
        };
      }(wrapper_.getAttribLocation);
    
      wrapper_.getVertexAttribOffset = function(f) {
        return function() {
          if (contextLost_) {
            return 0;
          }
          return f.apply(ctx, arguments);
        };
      }(wrapper_.getVertexAttribOffset);
    
      wrapper_.isContextLost = function() {
        return contextLost_;
      };
    
      function wrapEvent(listener) {
        if (typeof(listener) == "function") {
          return listener;
        } else {
          return function(info) {
            listener.handleEvent(info);
          }
        }
      }
    
      wrapper_.registerOnContextLostListener = function(listener) {
        onLost_ = wrapEvent(listener);
      };
    
      wrapper_.registerOnContextRestoredListener = function(listener) {
        if (contextLost_) {
          nextOnRestored_ = wrapEvent(listener);
        } else {
          onRestored_ = wrapEvent(listener);
        }
      }
    
      return wrapper_;
    }
    
    return {
      /**
       * Initializes this module. Safe to call more than once.
       * @param {!WebGLRenderingContext} ctx A WebGL context. If
       *    you have more than one context it doesn't matter which one
       *    you pass in, it is only used to pull out constants.
       */
      'init': init,
    
      /**
       * Returns true or false if value matches any WebGL enum
       * @param {*} value Value to check if it might be an enum.
       * @return {boolean} True if value matches one of the WebGL defined enums
       */
      'mightBeEnum': mightBeEnum,
    
      /**
       * Gets an string version of an WebGL enum.
       *
       * Example:
       *   WebGLDebugUtil.init(ctx);
       *   var str = WebGLDebugUtil.glEnumToString(ctx.getError());
       *
       * @param {number} value Value to return an enum for
       * @return {string} The string version of the enum.
       */
      'glEnumToString': glEnumToString,
    
      /**
       * Converts the argument of a WebGL function to a string.
       * Attempts to convert enum arguments to strings.
       *
       * Example:
       *   WebGLDebugUtil.init(ctx);
       *   var str = WebGLDebugUtil.glFunctionArgToString('bindTexture', 0, gl.TEXTURE_2D);
       *
       * would return 'TEXTURE_2D'
       *
       * @param {string} functionName the name of the WebGL function.
       * @param {number} argumentIndx the index of the argument.
       * @param {*} value The value of the argument.
       * @return {string} The value as a string.
       */
      'glFunctionArgToString': glFunctionArgToString,
    
      /**
       * Given a WebGL context returns a wrapped context that calls
       * gl.getError after every command and calls a function if the
       * result is not NO_ERROR.
       *
       * You can supply your own function if you want. For example, if you'd like
       * an exception thrown on any GL error you could do this
       *
       *    function throwOnGLError(err, funcName, args) {
       *      throw WebGLDebugUtils.glEnumToString(err) + " was caused by call to" +
       *            funcName;
       *    };
       *
       *    ctx = WebGLDebugUtils.makeDebugContext(
       *        canvas.getContext("webgl"), throwOnGLError);
       *
       * @param {!WebGLRenderingContext} ctx The webgl context to wrap.
       * @param {!function(err, funcName, args): void} opt_onErrorFunc The function
       *     to call when gl.getError returns an error. If not specified the default
       *     function calls console.log with a message.
       */
      'makeDebugContext': makeDebugContext,
    
      /**
       * Given a WebGL context returns a wrapped context that adds 4
       * functions.
       *
       * ctx.loseContext:
       *   simulates a lost context event.
       *
       * ctx.restoreContext:
       *   simulates the context being restored.
       *
       * ctx.registerOnContextLostListener(listener):
       *   lets you register a listener for context lost. Use instead
       *   of addEventListener('webglcontextlostevent', listener);
       *
       * ctx.registerOnContextRestoredListener(listener):
       *   lets you register a listener for context restored. Use
       *   instead of addEventListener('webglcontextrestored',
       *   listener);
       *
       * @param {!WebGLRenderingContext} ctx The webgl context to wrap.
       */
      'makeLostContextSimulatingContext': makeLostContextSimulatingContext,
    
      /**
       * Resets a context to the initial state.
       * @param {!WebGLRenderingContext} ctx The webgl context to
       *     reset.
       */
      'resetToInitialState': resetToInitialState
    };
    
    }();